#region Using Statements
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
#endregion

namespace DirectZZT.Shared.Core
{
    /// <summary>
    /// A class that contains various possibilities to log anything.
    /// </summary>
    public sealed class Logging : ILogging
    {
        #region Constants

        private const string DEFAULT_LOGGING_ID = "Default";

        #endregion

        #region Singleton (Thread-safe implementation)

        private static object _lock = new object();
        private static volatile Dictionary<string, ILogging> _instance = new Dictionary<string, ILogging>();

        /// <summary>
        /// Returns the default <see cref="ILogging"/>-implementation that can be used to access the logging facility.
        /// </summary>
        /// <returns>The default <see cref="ILogging"/>-implementation that can be used to access the logging facility.</returns>
        public static ILogging GetLogging()
        {
            // the default logging is empty string
            return GetLogging(DEFAULT_LOGGING_ID);
        }

        /// <summary>
        /// Returns the <see cref="ILogging"/> instance registered under the given id, or creates a new one.
        /// </summary>
        /// <param name="id">The id of the <see cref="ILogging"/> instance to return or to create.</param>
        /// <returns></returns>
        public static ILogging GetLogging(string id)
        {
            lock (_lock)
            {
                if (!_instance.ContainsKey(id))
                {
                    _instance[id] = new Logging();
                }
                return _instance[id];
            }
        }

        /// <summary>
        /// Returns an array with the identifiers of all registered logging instances.
        /// </summary>
        /// <returns></returns>
        public static string[] GetRegisteredLoggings()
        {
            return _instance.Keys.ToArray();
        }

        /// <summary>
        /// Shuts down all <see cref="ILogging"/> instances.
        /// </summary>
        public static void Shutdown()
        {
            lock (_lock)
            {
                while (_instance.Count > 0)
                {
                    // call shutdown method
                    _instance.First().Value.Shutdown();
                    // remove it
                    _instance.Remove(_instance.First().Key);
                }
            }
        }

        /// <summary>
        /// Shuts down the <see cref="ILogging"/> instance with the given id.
        /// </summary>
        /// <param name="id"></param>
        public static void ShutdownLogging(string id)
        {
            lock (_lock)
            {
                if (!_instance.ContainsKey(id))
                {
                    return;
                }
            }

            GetLogging(id).Shutdown();
            lock (_lock)
            {
                _instance.Remove(id);
            }
        }

        #endregion

        #region Constants

        /// <summary>
        /// Defines the Alias for the first log entry.
        /// </summary>
        private const int StartId = 1;
        /// <summary>
        /// Defines the directory in which log files are being stored.
        /// </summary>
        public static readonly string LogFileDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Assembly.GetExecutingAssembly().GetCompany());
        /// <summary>
        /// Defines the full path to the file in which the Logging Text is stored.
        /// This path is inside the current user's application data directory, where it is clear that he has access rights.
        /// This static field is read-only.
        /// </summary>
        public static readonly string LogFile = Path.Combine(LogFileDirectory, Assembly.GetExecutingAssembly().GetTitle() + ".log");

        #endregion

        #region Fields

        private int _cacheSize = 0;
        private List<LogEntry> _cacheCurrent;
        private List<ILoggingListener> _listeners;

        #endregion

        #region Properties

        /// <summary>
        /// Gets whether or not cache mode is active.
        /// </summary>
        public bool IsCacheMode
        {
            get { return _cacheSize > 0; }
        }

        /// <summary>
        /// Gets the current log entry alias.
        /// </summary>
        private int CurrentId { get; set; }

        /// <summary>
        /// Gets the file of the output file where the logs are stored.
        /// This file must be absolute.
        /// </summary>
        public string OutputFileName { get; private set; }

        /// <summary>
        /// Gets whether logging to the output file is enabled.
        /// </summary>
        /// <returns>true if we are on PC, false if we are on the 360.</returns>
        public static bool IsLoggingToOutputFileEnabled
        {
            get
            {
#if !XBOX
                return true;
#else
                return false;
#endif
            }
        }

        /// <summary>
        /// Gets/sets whether or not the logging of messages classified as "Debug" shall be logged, even if not compiled in DEBUG mode.
        /// </summary>
        public static bool ForceDebugMessageLogging { get; set; }

        /// <summary>
        /// Gets/sets the max file size of the log file before it gets cleaned up.
        /// </summary>
        public int MaxFileSize { get; set; }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the Logging class.
        /// </summary>
        private Logging()
        {
            // 512 KB are enough
            MaxFileSize = (int)(0.5f * 1024.0f * 1024.0f);

            _cacheCurrent = new List<LogEntry>(_cacheSize);
            CurrentId = StartId;
            // pre-set the output file name
            ((ILogging)this).SetOutputFileName(LogFile);

            _listeners = new List<ILoggingListener>();
        }

        #endregion

        #region Logging Method

        /// <summary>
        /// Writes the given text lines to the file.
        /// </summary>
        /// <param name="textLines">The text lines to write to the file.</param>
        private void WriteToFile(params string[] textLines)
        {
            lock (_lock)
            {
                // open the file stream in a way that it can still be read from (shared)
                using (FileStream stream = new FileStream(OutputFileName, FileMode.Append, FileAccess.Write, FileShare.Read))
                {
                    using (StreamWriter writer = new StreamWriter(stream))
                    {
                        foreach (string line in textLines)
                        {
                            writer.WriteLine(line);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Does the actual logging.
        /// </summary>
        /// <param name="entry">The <see cref="LogEntry"/> to log.</param>
        private void LogCore(LogEntry entry)
        {
            bool isConsole = (entry.MessageType == LoggingMessageType.Console);
            bool isNothing = (entry.MessageType == LoggingMessageType.None);

#if !DEBUG
            // ignore designated DEBUG-only messages (they might contain information that the user does not necessarily need to know)
            // however this does not apply if the user wants these messages to be logged (this might be due to a debug switch argument)
            if (entry.MessageType == LoggingMessageType.Debug && !ForceDebugMessageLogging)
            {
                return;
            }
#endif
            // auto-increment the alias of the entry, if not set yet
            if (entry.Id == null || entry.Id == 0)
            {
                entry.Id = CurrentId++;
            }

            // is logging to output file enabled?
            // this is only allowed when this is no console message
            if (!isConsole && IsLoggingToOutputFileEnabled)
            {
                // To Cache, or not To Cache, that's the question
                if ((_cacheSize > 0)
                    && (_cacheCurrent.Count <= _cacheSize))
                {
                    _cacheCurrent.Add(entry);
                }
                else
                {
                    _cacheCurrent.Add(entry);
                    ((ILogging)this).Flush();
                }
            }

            if (!isNothing)
            {
                // update our listeners
                // this goes asynchronous
                ThreadPool.QueueUserWorkItem(o =>
                {
                    lock (_listeners)
                    {
                        foreach (ILoggingListener listener in _listeners)
                        {
                            listener.Write(entry, false);
                        }
                    }
                });
            }
        }

        /// <summary>
        /// Flushes the most recent logging history by writing all cached entries to the file and clearing the cache.
        /// </summary>
        void ILogging.Flush()
        {
            lock (_cacheCurrent)
            {
                // check for max file size
                if (File.Exists(OutputFileName))
                {
                    FileInfo fi = new FileInfo(OutputFileName);
                    if (fi.Length >= MaxFileSize)
                    {
                        try
                        {
                            fi.Delete();
                        }
                        catch (Exception)
                        {
                            // silently catch exceptions in here
                        }
                    }
                }

                List<string> entries = new List<string>(_cacheCurrent.Count);
                // alright, whe have cached it up long enough, not write it
                while (_cacheCurrent.Count > 0)
                {
                    entries.Add(_cacheCurrent[0].ToString());
                    _cacheCurrent.RemoveAt(0);
                }
                WriteToFile(entries.ToArray());
                _cacheCurrent.Clear();
            }
        }

        /// <summary>
        /// Log a message.
        /// </summary>
        /// <param name="type">What kind of message type you want to log.</param>
        /// <param name="source">The name of the component that has raised the message.</param>
        /// <param name="text">The text to be logged.</param>
        private void Log(LoggingMessageType type, string source, string text)
        {
            LogCore(new LogEntry(type, source, text));
        }

        /// <summary>
        /// Special logging method which logs an exception.
        /// </summary>
        /// <param name="component">The name of the component that has raised the message.</param>
        /// <param name="exception">The exception to log.</param>
        void ILogging.LogException(string component, Exception exception)
        {
            LogEntry entry = new LogEntry(LoggingMessageType.Exception, component, exception.Message);
            entry.Exception = exception;
            LogCore(entry);
        }

        void ILogging.Log(LoggingMessageType type, string source, string category, string message)
        {
            LogEntry entry = new LogEntry(type, source, message, DateTime.Now);
            entry.Category = category;
            LogCore(entry);
        }

        void ILogging.LogFormat(LoggingMessageType type, string source, string category, string format, params object[] arguments)
        {
            Log(type, source, String.Format(CultureInfo.InvariantCulture, format, arguments));
        }

        LogEntry[] ILogging.GetEntries(Nullable<DateTime> start)
        {
            List<LogEntry> entries = new List<LogEntry>(64);

            string content = "";
            // lock as little as possible
            if (File.Exists(OutputFileName))
            {
                lock (_lock)
                {
                    using (FileStream stream = new FileStream(OutputFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                    {
                        using (StreamReader reader = new StreamReader(stream))
                        {
                            content = reader.ReadToEnd();
                        }
                    }
                }

                foreach (var line in content.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
                {
                    LogEntry entry = LogEntry.CreateFromString(line);
                    if (entry == null || (start.HasValue && (entry.Timestamp < start.Value)))
                    {
                        continue;
                    }
                    entries.Insert(0, entry);
                }
            }

            return entries.ToArray();
        }

        /// <summary>
        /// Registers a new <see cref="ILoggingListener"/>.
        /// </summary>
        /// <param name="listener">The <see cref="ILoggingListener"/> to register.</param>
        void ILogging.RegisterListener(ILoggingListener listener)
        {
            lock (_listeners)
            {
                _listeners.Add(listener);
            }
        }

        /// <summary>
        /// Unregisters an existing <see cref="ILoggingListener"/>.
        /// </summary>
        /// <param name="listener">The <see cref="ILoggingListener"/> to unregister.</param>
        void ILogging.UnregisterListener(ILoggingListener listener)
        {
            lock (_listeners)
            {
                _listeners.Remove(listener);
            }
        }

        /// <summary>
        /// Enables/disables caching mode.
        /// See documentation for further information.
        /// </summary>
        /// <remarks>When cache mode is enabled, then the logging will not write to the file on each log entry, but rather cache up the log entries,
        /// until they reach the limit set in <paramref name="cacheSize"/>. Only when reaching this limit, the writer will flush.</remarks>
        /// <param name="cacheSize">The amount of log entries to cache until the entries will be flushed.</param>
        void ILogging.SetCacheMode(int cacheSize)
        {
            // we need to flush here always
            ((ILogging)this).Flush();

            // set new cache mode
            _cacheSize = cacheSize;

        }

        /// <summary>
        /// Sets the output file.
        /// Next time something is logged it goes into the file given here.
        /// </summary>
        /// <param name="fileName">The new output file.</param>
        /// <exception cref="System.ArgumentException">Parameter <paramref name="fileName"/> was a file that was not absolute.</exception>
        void ILogging.SetOutputFileName(string fileName)
        {
            if (!Path.IsPathRooted(fileName))
            {
                throw new ArgumentException("File name must be absolute!", "fileName");
            }
            else
            {
                string dir = Path.GetDirectoryName(fileName);
                if (!Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }
                OutputFileName = fileName;
            }
        }

        void ILogging.AddBlankLine()
        {
            LogCore(new LogEntry(LoggingMessageType.None, String.Empty, String.Empty));
        }

        void ILogging.AddSeparator()
        {
            LogCore(new LogEntry(LoggingMessageType.None, String.Empty, "--------------------------------------------------------------------------------------"));
        }

        /// <summary>
        /// Removes the underlying log file and returns the result.
        /// </summary>
        /// <returns>
        /// <para>- The result of the deleting operation.</para>
        /// <para>- On Xbox 360, returns always 'true'.</para></returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        bool ILogging.DeleteLogFile()
        {
#if WINDOWS || MONO
            if (File.Exists(OutputFileName))
            {
                try
                {
                    File.Delete(OutputFileName);
                    return true;
                }
                catch
                {
                    // this case should never occur
                    return false;
                }
            }
            return false;
#else
            // if 'false' is returned that could be something some components may not like
            return true;
#endif
        }

        /// <summary>
        /// Initializes this logging instance.
        /// </summary>
        void ILogging.Initialize()
        {

        }

        /// <summary>
        /// Called when the host application is shutting down. Used to clean-up the caches (if any), finish writing to the hard disk, etc.
        /// </summary>
        void ILogging.Shutdown()
        {
            ((ILogging)this).Flush();

            // shutdown and detach all listeners that are still alive
            lock (_listeners)
            {
                while (_listeners.Count > 0)
                {
                    // shut down this listener and remove it
                    _listeners[0].Shutdown();
                    _listeners.RemoveAt(0);
                }
            }
        }

        #endregion
    }
}